package org.oos.dao;

import static org.oos.dao.options.OptionsMap.FINDERS.PAGE_OPTIONS;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

import org.oos.Constants;
import org.oos.atom.AtomEntity;
import org.oos.atom.AtomFeed;
import org.oos.atom.AtomService;
import org.oos.atom.AtomService.HttpStatusException;
import org.oos.auth.AuthenticationHandler;
import org.oos.dao.exceptions.CreationException;
import org.oos.dao.exceptions.DeleteException;
import org.oos.dao.exceptions.ReadException;
import org.oos.dao.exceptions.UpdateException;
import org.oos.dao.options.OptionsMap;
import org.oos.dao.options.PageOptions;
import org.oos.lists.DynamicList;
import org.oos.logging.ConsoleLogger;

import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.InputStreamContent;
import com.google.api.client.http.UrlEncodedContent;
import com.google.api.client.xml.XmlHttpParser;
import com.google.api.client.xml.XmlNamespaceDictionary;
import com.google.api.client.xml.atom.AtomContent;
import com.google.api.client.xml.atom.AtomParser;

/**
 * This abstract implementation of {@link Dao} provides the basic independent
 * processing that is extended by subclasses. A sub-class must provide with:
 * <ul>
 * <li>
 * The Class type for the feed that will be processed on read</li>
 * <li>
 * The URL to get the feed</li>
 * <li>
 * The URL to interact with an entry</li>
 * <li>
 * A method to convert a Feed into a DynamicList</li>
 * <li>
 * Whether the DAO requires authorization or not</li>
 * </ul>
 * 
 * @author pablo.gonzalez@11870.com
 * @see Dao
 * @see AtomEntity
 * @see AtomFeed
 * @see SiteDao
 * 
 */
public abstract class AbstractDao<Entry extends AtomEntity, Feed extends AtomFeed<Entry>>
		implements Dao<Entry, Feed> {

	protected Logger log = Logger.getLogger("DAO");
	private Feed feed;
	private DynamicList<Entry> list;
	private List<OptionsMap> options;
	protected AuthenticationHandler authHandler;

	public Class<Feed> feedType;
	private Class<Entry> entryType;

	@SuppressWarnings("unchecked")
	AbstractDao(Class<?> feedType, Class<?> entryType) {
		try {
			this.feedType = (Class<Feed>) feedType;
			this.entryType = (Class<Entry>) entryType;
		} catch (ClassCastException e) {
			throw new IllegalArgumentException(
					"This Dao must provide with a valid AtomFeed/AtomEntity "
							+ "as the arguments of the super constructors");
		}
	}

	@Override
	public Dao<Entry, Feed> setAuthenticationHandler(
			AuthenticationHandler authHandler) {
		this.authHandler = authHandler;
		return this;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.oos.dao.Dao#setLog(java.util.logging.Logger)
	 */
	@Override
	public Dao<Entry, Feed> setLog(Logger log) {
		this.log = log;
		return this;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.oos.dao.Dao#getLog()
	 */
	@Override
	public Logger getLog() {
		if (log == null) {
			log = new ConsoleLogger();
		}
		return log;
	}

	/**
	 * @return A valid {@link HttpTransport} and signs it if required.
	 */
	public HttpTransport createTransport() {
		HttpTransport transport = new HttpTransport();
		XmlHttpParser parser;
		if (getContentType() != null) {
			parser = new XmlHttpParser();
			parser.contentType = getContentType();
			parser.namespaceDictionary = getPlainXmlNamespaceDictionary();
		} else {
			parser = new AtomParser();
			parser.namespaceDictionary = getXmlNamespaceDictionary();
		}
		transport.addParser(parser);

		return transport;
	}

	/**
	 * Override this method to provide a different content type.
	 * 
	 * @return
	 */
	protected String getContentType() {
		return null;
	}

	/**
	 * Override this method to provide with a different
	 * {@link XmlNamespaceDictionary}
	 * 
	 * @return
	 */
	protected XmlNamespaceDictionary getXmlNamespaceDictionary() {
		return Constants.OOS_NAMESPACE_DICTIONARY;
	}
	
	protected XmlNamespaceDictionary getPlainXmlNamespaceDictionary() {
		XmlNamespaceDictionary dictionary = new XmlNamespaceDictionary();
		Map<String, String> map = Constants.OOS_NAMESPACE_DICTIONARY.namespaceAliasToUriMap;
		for (String key : map.keySet()) {
			dictionary.addNamespace(key, map.get(key));
		}
		dictionary.namespaceAliasToUriMap.remove("");
		return dictionary;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.oos.dao.Dao#read()
	 */
	@SuppressWarnings("unchecked")
	@Override
	public <L extends DynamicList<Entry>> L read() throws ReadException {
		if (feed == null) {
			feed = readFeed(1);
			list = getList(feed);
		}
		return (L) list;
	}
	
//	@Override
//	public Feed readFeed(Integer start, Integer end) throws ReadException {
//		PageOptions pageOptions = PAGE_OPTIONS.get(getOptions());
//		int originalPagesize = pageOptions.getPageSize();
//		try {
//			pageOptions.setPage(start);
//			pageOptions.setPageSize(originalPagesize * (end - start));
//			Feed feed = AtomService.executeGet(authHandler, createTransport(),
//					getFeedUrl(), getOptions(), getFeedType());
//			return feed;
//		} catch (Exception e) {
//			throw new ReadException(e);
//		} finally {
//			pageOptions.setPageSize(originalPagesize);
//		}
//	}

	@Override
	public Feed readFeed(Integer index) throws ReadException {
		try {
			PageOptions pageOptions = PAGE_OPTIONS.get(getOptions());
			pageOptions.setPage(index);
			Feed feed = AtomService.executeGet(authHandler, createTransport(),
					getFeedUrl(), getOptions(), getFeedType());
			return feed;
		} catch (Exception e) {
			throw new ReadException(e);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.oos.dao.Dao#read(java.lang.Integer)
	 */
	@Override
	public Entry read(Integer index) throws ReadException {
		return read().get(index);
	}
	
	@Override
	public Entry readEntry(String param) throws ReadException {
		try {
			Entry entry = AtomService.executeGet(authHandler, createTransport(),
					getEntryUrl(param), getOptions(), getEntryType());
			return entry;
		} catch (HttpStatusException e) {
			if (e.getStatusCode() == 404) {
				return null;
			}
			throw new ReadException(e);
		} catch (Exception e) {
			throw new ReadException(e);
		}
	}
	
	@Override
	public InputStream readMedia(String param) throws ReadException {
		try {
			HttpResponse response = AtomService.executeRawGet(authHandler, createTransport(),
					getMediaUrl(param), getOptions());
			return response.getContent();
		} catch (Exception e) {
			throw new ReadException(e);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.oos.dao.Dao#create(org.oos.atom.AtomEntity)
	 */
	@Override
	public Entry create(Entry entry) throws CreationException {
		try {
			AtomContent content = new AtomContent();
			content.entry = entry;
			content.namespaceDictionary = getXmlNamespaceDictionary();
			return AtomService.executePost(authHandler, createTransport(), getFeedUrl(), content, getOptions(), getEntryType());
		} catch (Exception e) {
			throw new CreationException(e);
		}
	}

	@Override
	public Entry create() throws CreationException {
		try {
			UrlEncodedContent content =  new UrlEncodedContent();
			return AtomService.executePost(authHandler, createTransport(), getFeedUrl(), content, getOptions(), getEntryType());
		} catch (HttpStatusException e) {
			CreationException exception = new CreationException(e.getMessage());
			exception.setResponse(e.getResponse());
			throw exception;
		}catch (Exception e) {
			throw new CreationException(e);
		}
	}

	@Override
	public Entry createMedia(File file, String contentType) throws CreationException {
		try {
			InputStreamContent content = new InputStreamContent();
			content.inputStream = new FileInputStream(file);
			content.type = contentType;
			content.length = file.length();
			return AtomService.executePost(authHandler, createTransport(), getFeedUrl(), content, getOptions(), getEntryType());
		} catch (Exception e) {
			throw new CreationException(e);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.oos.dao.Dao#delete(org.oos.atom.AtomEntity)
	 */
	@Override
	public boolean delete(Entry entry) throws DeleteException {
		try {
			return AtomService.executeDelete(authHandler, createTransport(), getEntryUrl(entry));
		} catch (Exception e) {
			throw new DeleteException(e);
		}
	}
	
	@Override
	public boolean deleteMedia(Entry entry) throws DeleteException {
		try {
			return AtomService.executeDelete(authHandler, createTransport(), getMediaUrl(entry));
		} catch (Exception e) {
			throw new DeleteException(e);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.oos.dao.Dao#update(org.oos.atom.AtomEntity)
	 */
	@Override
	public boolean update(Entry entry) throws UpdateException {
		try {
			AtomContent content = new AtomContent();
			content.entry = entry;
			content.namespaceDictionary = getXmlNamespaceDictionary();
			return AtomService.executePut(authHandler, createTransport(), getEntryUrl(entry), content);
		} catch (Exception e) {
			throw new UpdateException(e);
		}
	}
	
	@Override
	public boolean updateMedia(Entry entry, File file, String contentType) throws UpdateException {
		try {
			InputStreamContent content = new InputStreamContent();
			content.inputStream = new FileInputStream(file);
			content.type = contentType;
			content.length = file.length();
			return AtomService.executePut(authHandler, createTransport(), getMediaUrl(entry), content);
		} catch (Exception e) {
			throw new UpdateException(e);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.oos.dao.Dao#size()
	 */
	@Override
	public int size() throws ReadException {
		return read().size();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.oos.dao.Dao#getOptions()
	 */
	@Override
	public List<OptionsMap> getOptions() {
		if (options == null) {
			options = new ArrayList<OptionsMap>();
		}
		return options;
	}

	/* Method to be implemented by concrete DAOs */

	/**
	 * It should return a {@link Class} of the type that is processed by the DAO
	 */
	@SuppressWarnings("unchecked")
	<F extends Feed> Class<F> getFeedType() {
		if (feedType == null) {
			throw new IllegalStateException("The Feed type must be set.");
		}
		return (Class<F>) feedType;
	}

	@SuppressWarnings("unchecked")
	<E extends Entry> Class<E> getEntryType() {
		if (feedType == null) {
			throw new IllegalStateException("The Entry type must be set.");
		}
		return (Class<E>) entryType;
	}

	/**
	 * @return The {@link OOSUrl} for interacting with the feed.
	 */
	protected abstract GenericUrl getFeedUrl();

	/**
	 * @return The {@link OOSUrl} for the interaction with an entry
	 */
	protected GenericUrl getEntryUrl(Entry entry) {
		throw new UnsupportedOperationException();
	}
	
	protected GenericUrl getEntryUrl(String param) {
		throw new UnsupportedOperationException();
	}
	
	protected GenericUrl getMediaUrl(Entry entry) {
		throw new UnsupportedOperationException();
	}
	
	protected GenericUrl getMediaUrl(String param) {
		throw new UnsupportedOperationException();
	}

	/**
	 * Converts the provided Feed to a {@link DynamicList}
	 * 
	 * @param <L>
	 * @param feed
	 * @return
	 */
	protected abstract <L extends DynamicList<Entry>> L getList(Feed feed);

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		return getFeedUrl().toString();
	}

}
